{
  "cells": [
    {
      "cell_type": "markdown",
      "metadata": {
        "id": "6bYaCABobL5q"
      },
      "source": [
        "##### Copyright 2021 The TensorFlow Authors."
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "metadata": {
        "cellView": "form",
        "id": "FlUw7tSKbtg4"
      },
      "outputs": [],
      "source": [
        "#@title Licensed under the Apache License, Version 2.0 (the \"License\");\n",
        "# you may not use this file except in compliance with the License.\n",
        "# You may obtain a copy of the License at\n",
        "#\n",
        "# https://www.apache.org/licenses/LICENSE-2.0\n",
        "#\n",
        "# Unless required by applicable law or agreed to in writing, software\n",
        "# distributed under the License is distributed on an \"AS IS\" BASIS,\n",
        "# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n",
        "# See the License for the specific language governing permissions and\n",
        "# limitations under the License."
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {
        "id": "MfBg1C5NB3X0"
      },
      "source": [
        "<table class=\"tfo-notebook-buttons\" align=\"left\">\n",
        "  <td>\n",
        "    <a target=\"_blank\" href=\"https://www.tensorflow.org/guide/migrate/validate_correctness\"><img src=\"https://www.tensorflow.org/images/tf_logo_32px.png\" />View on TensorFlow.org</a>\n",
        "  </td>\n",
        "  <td>\n",
        "    <a target=\"_blank\" href=\"https://colab.research.google.com/github/tensorflow/docs/blob/master/site/en/guide/migrate/validate_correctness.ipynb\"><img src=\"https://www.tensorflow.org/images/colab_logo_32px.png\" />Run in Google Colab</a>\n",
        "  </td>\n",
        "  <td>\n",
        "    <a target=\"_blank\" href=\"https://github.com/tensorflow/docs/blob/master/site/en/guide/migrate/validate_correctness.ipynb\"><img src=\"https://www.tensorflow.org/images/GitHub-Mark-32px.png\" />View on GitHub</a>\n",
        "  </td>\n",
        "  <td>\n",
        "    <a href=\"https://storage.googleapis.com/tensorflow_docs/docs/site/en/guide/migrate/validate_correctness.ipynb\"><img src=\"https://www.tensorflow.org/images/download_logo_32px.png\" />Download notebook</a>\n",
        "  </td>\n",
        "</table>"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {
        "id": "yAMJsAn7NDbc"
      },
      "source": [
        "# Validating correctness & numerical equivalence"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {
        "id": "vyddl2kckpdN"
      },
      "source": [
        "When migrating your TensorFlow code from TF1.x to TF2, it is a good practice to ensure that your migrated code behaves the same way in TF2 as it did in TF1.x. \n",
        "\n",
        "This guide covers migration code examples with the `tf.compat.v1.keras.utils.track_tf1_style_variables` modeling shim applied to `tf.keras.layers.Layer` methods. Read the [model mapping guide](./model_mapping.ipynb) to find out more about the TF2 modeling shims.\n",
        "\n",
        "This guide details approaches you can use to: \n",
        "* Validate the correctness of the results obtained from training models using the migrated code \n",
        "* Validate the numerical equivalence of your code across TensorFlow versions"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {
        "id": "TaYgaekzOAHf"
      },
      "source": [
        "## Setup"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "metadata": {
        "id": "FkHX044DzVsd"
      },
      "outputs": [],
      "source": [
        "!pip uninstall -y -q tensorflow"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "metadata": {
        "id": "M1ZgieHtyzKI"
      },
      "outputs": [],
      "source": [
        "# Install tf-nightly as the DeterministicRandomTestTool is available only in\n",
        "# Tensorflow 2.8\n",
        "!pip install -q tf-nightly"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "metadata": {
        "id": "ohYETq4NCX4J"
      },
      "outputs": [],
      "source": [
        "!pip install -q tf_slim"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "metadata": {
        "id": "MFey2HxcktP6"
      },
      "outputs": [],
      "source": [
        "import tensorflow as tf\n",
        "import tensorflow.compat.v1 as v1\n",
        "\n",
        "import numpy as np\n",
        "import tf_slim as slim\n",
        "import sys\n",
        "\n",
        "\n",
        "from contextlib import contextmanager"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "metadata": {
        "id": "OriidSSAmRtW"
      },
      "outputs": [],
      "source": [
        "!git clone --depth=1 https://github.com/tensorflow/models.git\n",
        "import models.research.slim.nets.inception_resnet_v2 as inception"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {
        "id": "TRacYNxnN-nk"
      },
      "source": [
        "If you're putting a nontrivial chunk of forward pass code into the shim, you want to know that it is behaving the same way as it did in TF1.x. For example, consider trying to put an entire TF-Slim Inception-Resnet-v2 model into the shim as such:"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "metadata": {
        "id": "IijQZtxeaErg"
      },
      "outputs": [],
      "source": [
        "# TF1 Inception resnet v2 forward pass based on slim layers\n",
        "def inception_resnet_v2(inputs, num_classes, is_training):\n",
        "  with slim.arg_scope(\n",
        "    inception.inception_resnet_v2_arg_scope(batch_norm_scale=True)):\n",
        "    return inception.inception_resnet_v2(inputs, num_classes, is_training=is_training)"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "metadata": {
        "id": "Z_-Oxg9OlSd4"
      },
      "outputs": [],
      "source": [
        "class InceptionResnetV2(tf.keras.layers.Layer):\n",
        "  \"\"\"Slim InceptionResnetV2 forward pass as a Keras layer\"\"\"\n",
        "\n",
        "  def __init__(self, num_classes, **kwargs):\n",
        "    super().__init__(**kwargs)\n",
        "    self.num_classes = num_classes\n",
        "\n",
        "  @tf.compat.v1.keras.utils.track_tf1_style_variables\n",
        "  def call(self, inputs, training=None):\n",
        "    is_training = training or False \n",
        "    \n",
        "    # Slim does not accept `None` as a value for is_training,\n",
        "    # Keras will still pass `None` to layers to construct functional models\n",
        "    # without forcing the layer to always be in training or in inference.\n",
        "    # However, `None` is generally considered to run layers in inference.\n",
        "    \n",
        "    with slim.arg_scope(\n",
        "        inception.inception_resnet_v2_arg_scope(batch_norm_scale=True)):\n",
        "      return inception.inception_resnet_v2(\n",
        "          inputs, self.num_classes, is_training=is_training)\n"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {
        "id": "EqFmpktjlvh9"
      },
      "source": [
        "As it so happens, this layer actually works perfectly fine out of the box (complete with accurate regularization loss tracking). \n",
        "\n",
        "However, this is not something you want to take for granted. Follow the below steps to verify that it is actually behaving as it did in TF1.x, down to observing perfect numerical equivalence. These steps can also help you triangulate what part of the forward pass is causing a divergence from TF1.x (identify if the divergence arises in the model forward pass as opposed to a different part of the model)."
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {
        "id": "mmgubd9vkevp"
      },
      "source": [
        "## Step 1: Verify variables are only created once\n",
        "\n",
        "The very first thing you should verify is that you have correctly built the model in a way that reuses variables in each call rather than accidentally creating and using new variables each time. For example, if your model creates a new Keras layer or calls `tf.Variable` in each forward pass call then it is most likely failing to capture variables and creating new ones each time.\n",
        "\n",
        "Below are two context manager scopes you can use to detect when your model is creating new variables and debug which part of the model is doing it."
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "metadata": {
        "id": "VMTfTXC0zW97"
      },
      "outputs": [],
      "source": [
        "@contextmanager\n",
        "def assert_no_variable_creations():\n",
        "  \"\"\"Assert no variables are created in this context manager scope.\"\"\"\n",
        "  def invalid_variable_creator(next_creator, **kwargs):\n",
        "    raise ValueError(\"Attempted to create a new variable instead of reusing an existing one. Args: {}\".format(kwargs))\n",
        "\n",
        "  with tf.variable_creator_scope(invalid_variable_creator):\n",
        "    yield\n",
        "\n",
        "@contextmanager\n",
        "def catch_and_raise_created_variables():\n",
        "  \"\"\"Raise all variables created within this context manager scope (if any).\"\"\"\n",
        "  created_vars = []\n",
        "  def variable_catcher(next_creator, **kwargs):\n",
        "    var = next_creator(**kwargs)\n",
        "    created_vars.append(var)\n",
        "    return var\n",
        "\n",
        "  with tf.variable_creator_scope(variable_catcher):\n",
        "    yield\n",
        "  if created_vars:\n",
        "    raise ValueError(\"Created vars:\", created_vars)"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {
        "id": "WOKUtciktQqv"
      },
      "source": [
        "The first scope (`assert_no_variable_creations()`) will raise an error immediately once you try creating a variable within the scope. This allows you to inspect the stacktrace (and use interactive debugging) to figure out exactly what lines of code created a variable instead of reusing an existing one.\n",
        "\n",
        "The second scope (`catch_and_raise_created_variables()`) will raise an exception at the end of the scope if any variables ended up being created. This exception will include the list of all variables created in the scope. This is useful for figuring out what the set of all weights your model is creating is in case you can spot general patterns. However, it is less useful for identifying the exact lines of code where those variables got created.\n",
        "\n",
        "Use both scopes below to verify that the shim-based InceptionResnetV2 layer does not create any new variables after the first call (presumably reusing them)."
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "metadata": {
        "id": "O9FAGotiuLbK"
      },
      "outputs": [],
      "source": [
        "model = InceptionResnetV2(1000)\n",
        "height, width = 299, 299\n",
        "num_classes = 1000\n",
        "\n",
        "inputs = tf.ones( (1, height, width, 3))\n",
        "# Create all weights on the first call\n",
        "model(inputs)\n",
        "\n",
        "# Verify that no new weights are created in followup calls\n",
        "with assert_no_variable_creations():\n",
        "  model(inputs)\n",
        "with catch_and_raise_created_variables():\n",
        "  model(inputs)"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {
        "id": "9ylT-EIhu1lK"
      },
      "source": [
        "In the example below, observe how these decorators work on a layer that incorrectly creates new weights each time instead of reusing existing ones."
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "metadata": {
        "id": "gXqhPQWWtMAw"
      },
      "outputs": [],
      "source": [
        "class BrokenScalingLayer(tf.keras.layers.Layer):\n",
        "  \"\"\"Scaling layer that incorrectly creates new weights each time:\"\"\"\n",
        "\n",
        "  @tf.compat.v1.keras.utils.track_tf1_style_variables\n",
        "  def call(self, inputs):\n",
        "    var = tf.Variable(initial_value=2.0)\n",
        "    bias = tf.Variable(initial_value=2.0, name='bias')\n",
        "    return inputs * var + bias"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "metadata": {
        "id": "ztUKlMdGvHSq"
      },
      "outputs": [],
      "source": [
        "model = BrokenScalingLayer()\n",
        "inputs = tf.ones( (1, height, width, 3))\n",
        "model(inputs)\n",
        "\n",
        "try:\n",
        "  with assert_no_variable_creations():\n",
        "    model(inputs)\n",
        "except ValueError as err:\n",
        "  import traceback\n",
        "  traceback.print_exc()\n"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "metadata": {
        "id": "6VyfMJ50vZqZ"
      },
      "outputs": [],
      "source": [
        "model = BrokenScalingLayer()\n",
        "inputs = tf.ones( (1, height, width, 3))\n",
        "model(inputs)\n",
        "\n",
        "try:\n",
        "  with catch_and_raise_created_variables():\n",
        "    model(inputs)\n",
        "except ValueError as err:\n",
        "  print(err)"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {
        "id": "JDaiTArcv49M"
      },
      "source": [
        "You can fix the layer by making sure it only creates the weights once and then reuses them each time."
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "metadata": {
        "id": "FN1Oa10iviv8"
      },
      "outputs": [],
      "source": [
        "class FixedScalingLayer(tf.keras.layers.Layer):\n",
        "  \"\"\"Scaling layer that incorrectly creates new weights each time:\"\"\"\n",
        "  def __init__(self):\n",
        "    super().__init__()\n",
        "    self.var = None\n",
        "    self.bias = None\n",
        "\n",
        "  @tf.compat.v1.keras.utils.track_tf1_style_variables\n",
        "  def call(self, inputs):\n",
        "    if self.var is None:\n",
        "      self.var = tf.Variable(initial_value=2.0)\n",
        "      self.bias = tf.Variable(initial_value=2.0, name='bias')\n",
        "    return inputs * self.var + self.bias\n",
        "\n",
        "model = FixedScalingLayer()\n",
        "inputs = tf.ones( (1, height, width, 3))\n",
        "model(inputs)\n",
        "\n",
        "with assert_no_variable_creations():\n",
        "  model(inputs)\n",
        "with catch_and_raise_created_variables():\n",
        "  model(inputs)"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {
        "id": "MuiZZ7ktwCcn"
      },
      "source": [
        "### Troubleshooting\n",
        "\n",
        "Here are some common reasons why your model might accidentally be creating new weights instead of reusing existing ones:\n",
        "\n",
        "1. It uses an explicit `tf.Variable` call without reusing already-created `tf.Variables`. Fix this by first checking if it has not been created then reusing the existing ones.\n",
        "2. It creates a Keras layer or model directly in the forward pass each time (as opposed to `tf.compat.v1.layers`). Fix this by first checking if it has not been created then reusing the existing ones.\n",
        "3. It is built on top of `tf.compat.v1.layers` but fails to assign all `compat.v1.layers` an explicit name or to wrap your `compat.v1.layer` usage inside of a named `variable_scope`, causing the autogenerated layer names to increment in each model call. Fix this by putting a named `tf.compat.v1.variable_scope` inside your shim-decorated method that wraps all of your `tf.compat.v1.layers` usage."
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {
        "id": "V4iZLV9BnwKM"
      },
      "source": [
        "## Step 2: Check that variable counts, names, and shapes match\n",
        "\n",
        "The second step is to make sure your layer running in TF2 creates the same number of weights, with the same shapes, as the corresponding code does in TF1.x.\n",
        "\n",
        "You can do a mix of manually checking them to see that they match, and doing the checks programmatically in a unit test as shown below."
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "metadata": {
        "id": "m_aqag5fpun5"
      },
      "outputs": [],
      "source": [
        "# Build the forward pass inside a TF1.x graph, and \n",
        "# get the counts, shapes, and names of the variables\n",
        "graph = tf.Graph()\n",
        "with graph.as_default(), tf.compat.v1.Session(graph=graph) as sess:\n",
        "  height, width = 299, 299\n",
        "  num_classes = 1000\n",
        "  inputs = tf.ones( (1, height, width, 3))\n",
        "\n",
        "  out, endpoints = inception_resnet_v2(inputs, num_classes, is_training=False)\n",
        "\n",
        "  tf1_variable_names_and_shapes = {\n",
        "      var.name: (var.trainable, var.shape) for var in tf.compat.v1.global_variables()}\n",
        "  num_tf1_variables = len(tf.compat.v1.global_variables())"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {
        "id": "WT1-cm99vfNU"
      },
      "source": [
        "Next, do the same for the shim-wrapped layer in TF2.\n",
        "Notice that the model is also called multiple times before grabbing the weights. This is done to effectively test for variable reuse."
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "metadata": {
        "id": "S7ND-lBSqmnE"
      },
      "outputs": [],
      "source": [
        "height, width = 299, 299\n",
        "num_classes = 1000\n",
        "\n",
        "model = InceptionResnetV2(num_classes)\n",
        "# The weights will not be created until you call the model\n",
        "\n",
        "inputs = tf.ones( (1, height, width, 3))\n",
        "# Call the model multiple times before checking the weights, to verify variables\n",
        "# get reused rather than accidentally creating additional variables\n",
        "out, endpoints = model(inputs, training=False)\n",
        "out, endpoints = model(inputs, training=False)\n",
        "\n",
        "# Grab the name: shape mapping and the total number of variables separately,\n",
        "# because in TF2 variables can be created with the same name\n",
        "num_tf2_variables = len(model.variables)\n",
        "tf2_variable_names_and_shapes = {\n",
        "    var.name: (var.trainable, var.shape) for var in model.variables}"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "metadata": {
        "id": "pY2P_4wqsOYw"
      },
      "outputs": [],
      "source": [
        "# Verify that the variable counts, names, and shapes all match:\n",
        "assert num_tf1_variables == num_tf2_variables\n",
        "assert tf1_variable_names_and_shapes == tf2_variable_names_and_shapes"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {
        "id": "N4YKJzSVwWkc"
      },
      "source": [
        "The shim-based InceptionResnetV2 layer passes this test. However, in the case where they don't match, you can run it through a diff (text or other) to see where the differences are.\n",
        "\n",
        "This can provide a clue as to what part of the model isn't behaving as expected. With eager execution you can use pdb, interactive debugging, and breakpoints to dig into the parts of the model that seem suspicious, and debug what is going wrong in more depth."
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {
        "id": "2gYrt-_0xpRM"
      },
      "source": [
        "### Troubleshooting\n",
        "\n",
        "* Pay close attention to the names of any variables created directly by explicit `tf.Variable` calls and Keras layers/models as their variable name generation semantics may differ slightly between TF1.x graphs and TF2 functionality such as eager execution and `tf.function` even if everything else is working properly. If this is the case for you, adjust your test to account for any slightly different naming semantics.\n",
        "\n",
        "* You may sometimes find that the `tf.Variable`s, `tf.keras.layers.Layer`s, or `tf.keras.Model`s created in your training loop's forward pass are missing from your TF2 variables list even if they were captured by the variables collection in TF1.x. Fix this by assigning the variables/layers/models that your forward pass creates to instance attributes in your model. See [here](https://www.tensorflow.org/guide/keras/custom_layers_and_models) for more info."
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {
        "id": "fOQJ_hUGnzkq"
      },
      "source": [
        "## Step 3: Reset all variables, check numerical equivalence with all randomness disabled\n",
        "\n",
        "The next step is to verify numerical equivalence for both the actual outputs and the regularization loss tracking when you fix the model such that there is no random number generation involved (such as during inference).\n",
        "\n",
        "The exact way to do this may depend on your specific model, but in most models (such as this one), you can do this by:\n",
        "1. Initializing the weights to the same value with no randomness. This can be done by resetting them to a fixed value after they have been created.\n",
        "2. Running the model in inference mode to avoid triggering any dropout layers which can be sources of randomness.\n",
        "\n",
        "The following code demonstrates how you can compare the TF1.x and TF2 results this way."
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "metadata": {
        "id": "kL4PzD2Cxzmp"
      },
      "outputs": [],
      "source": [
        "graph = tf.Graph()\n",
        "with graph.as_default(), tf.compat.v1.Session(graph=graph) as sess:\n",
        "  height, width = 299, 299\n",
        "  num_classes = 1000\n",
        "  inputs = tf.ones( (1, height, width, 3))\n",
        "\n",
        "  out, endpoints = inception_resnet_v2(inputs, num_classes, is_training=False)\n",
        "\n",
        "  # Rather than running the global variable initializers,\n",
        "  # reset all variables to a constant value\n",
        "  var_reset = tf.group([var.assign(tf.ones_like(var) * 0.001) for var in tf.compat.v1.global_variables()])\n",
        "  sess.run(var_reset)\n",
        "\n",
        "  # Grab the outputs & regularization loss\n",
        "  reg_losses = tf.compat.v1.get_collection(tf.compat.v1.GraphKeys.REGULARIZATION_LOSSES)\n",
        "  tf1_regularization_loss = sess.run(tf.math.add_n(reg_losses))\n",
        "  tf1_output = sess.run(out)\n",
        "\n",
        "print(\"Regularization loss:\", tf1_regularization_loss)\n",
        "tf1_output[0][:5]"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {
        "id": "IKkoM_x72rUa"
      },
      "source": [
        "Get the TF2 results."
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "metadata": {
        "id": "kb086gJwzsNo"
      },
      "outputs": [],
      "source": [
        "height, width = 299, 299\n",
        "num_classes = 1000\n",
        "\n",
        "model = InceptionResnetV2(num_classes)\n",
        "\n",
        "inputs = tf.ones((1, height, width, 3))\n",
        "# Call the model once to create the weights\n",
        "out, endpoints = model(inputs, training=False)\n",
        "\n",
        "# Reset all variables to the same fixed value as above, with no randomness\n",
        "for var in model.variables:\n",
        "  var.assign(tf.ones_like(var) * 0.001)\n",
        "tf2_output, endpoints = model(inputs, training=False)\n",
        "\n",
        "# Get the regularization loss\n",
        "tf2_regularization_loss = tf.math.add_n(model.losses)\n",
        "\n",
        "print(\"Regularization loss:\", tf2_regularization_loss)\n",
        "tf2_output[0][:5]"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "metadata": {
        "id": "CUfWqlgIK6ej"
      },
      "outputs": [],
      "source": [
        "# Create a dict of tolerance values\n",
        "tol_dict={'rtol':1e-06, 'atol':1e-05}"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "metadata": {
        "id": "R-C07eTo0WTr"
      },
      "outputs": [],
      "source": [
        "# Verify that the regularization loss and output both match\n",
        "# when we fix the weights and avoid randomness by running inference:\n",
        "np.testing.assert_allclose(tf1_regularization_loss, tf2_regularization_loss.numpy(), **tol_dict)\n",
        "np.testing.assert_allclose(tf1_output, tf2_output.numpy(), **tol_dict)"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {
        "id": "5UUq_Fuc2zDO"
      },
      "source": [
        "The numbers match between TF1.x and TF2 when you remove sources of randomness, and the TF2-compatible `InceptionResnetV2` layer passes the test.\n",
        "\n",
        "If you are observing the results diverging for your own models, you can use printing or pdb and interactive debugging to identify where and why the results start to diverge. Eager execution can make this significantly easier. You can also use an ablation approach to run only small portions of the model on fixed intermediate inputs and isolate where the divergence happens.\n",
        "\n",
        "Conveniently, many slim nets (and other models) also expose intermediate endpoints that you can probe."
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {
        "id": "btRbak-0ou15"
      },
      "source": [
        "## Step 4: Align random number generation, check numerical equivalence in both training and inference\n",
        "\n",
        "The final step is to verify that the TF2 model numerically matches the TF1.x model, even when accounting for random number generation in variable initialization and in the forward pass itself (such as dropout layers during the forward pass).\n",
        "\n",
        "You can do this by using the testing tool below to make random number generation semantics match between TF1.x graphs/sessions and eager execution."
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {
        "id": "jYq-JHiC39QC"
      },
      "source": [
        "TF1 legacy graphs/sessions and TF2 eager execution use different stateful random number generation semantics.\n",
        "\n",
        "In `tf.compat.v1.Session`s, if no seeds are specified, the random number generation depends on how many operations are in the graph at the time when the random operation is added, and how many times the graph is run. In eager execution, stateful random number generation depends on the global seed, the operation random seed, and how many times the operation with the operation with the given random seed is run. See \n",
        "`tf.random.set_seed` for more info."
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {
        "id": "BQbb8Hyk5YVi"
      },
      "source": [
        "The following [`v1.keras.utils.DeterministicRandomTestTool`](https://www.tensorflow.org/api_docs/python/tf/compat/v1/keras/utils/DeterministicRandomTestTool) class provides a context manager `scope()` that can make stateful random operations use the same seed across both TF1 graphs/sessions and eager execution.\n",
        "\n",
        "The tool provides two testing modes: \n",
        "1. `constant` which uses the same seed for every single operation no matter how many times it has been called and,\n",
        "2. `num_random_ops` which uses the number of previously-observed stateful random operations as the operation seed.\n",
        "\n",
        "This applies both to the stateful random operations used for creating and initializing variables, and to the stateful random operations used in computation (such as for dropout layers)."
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {
        "id": "MoyZenhGHDA-"
      },
      "source": [
        "Generate three random tensors to show how to use this tool to make stateful random number generation match between sessions and eager execution."
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "metadata": {
        "id": "DDFfjrbXEWED"
      },
      "outputs": [],
      "source": [
        "random_tool = v1.keras.utils.DeterministicRandomTestTool()\n",
        "with random_tool.scope():\n",
        "  graph = tf.Graph()\n",
        "  with graph.as_default(), tf.compat.v1.Session(graph=graph) as sess:\n",
        "    a = tf.random.uniform(shape=(3,1))\n",
        "    a = a * 3\n",
        "    b = tf.random.uniform(shape=(3,3))\n",
        "    b = b * 3\n",
        "    c = tf.random.uniform(shape=(3,3))\n",
        "    c = c * 3\n",
        "    graph_a, graph_b, graph_c = sess.run([a, b, c])\n",
        "\n",
        "graph_a, graph_b, graph_c"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "metadata": {
        "id": "o9bkdPuTFpYr"
      },
      "outputs": [],
      "source": [
        "random_tool = v1.keras.utils.DeterministicRandomTestTool()\n",
        "with random_tool.scope():\n",
        "  a = tf.random.uniform(shape=(3,1))\n",
        "  a = a * 3\n",
        "  b = tf.random.uniform(shape=(3,3))\n",
        "  b = b * 3\n",
        "  c = tf.random.uniform(shape=(3,3))\n",
        "  c = c * 3\n",
        "\n",
        "a, b, c"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "metadata": {
        "id": "qRJYFydsGIbF"
      },
      "outputs": [],
      "source": [
        "# Demonstrate that the generated random numbers match\n",
        "np.testing.assert_allclose(graph_a, a.numpy(), **tol_dict)\n",
        "np.testing.assert_allclose(graph_b, b.numpy(), **tol_dict)\n",
        "np.testing.assert_allclose(graph_c, c.numpy(), **tol_dict)"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {
        "id": "J8IWCnS-WFrB"
      },
      "source": [
        "However, notice that in `constant` mode, because `b` and `c` were generated with the same seed and have the same shape, they will have exactly the same values."
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "metadata": {
        "id": "IdxV89q2WPid"
      },
      "outputs": [],
      "source": [
        "np.testing.assert_allclose(b.numpy(), c.numpy(), **tol_dict)"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {
        "id": "vQTm7joHHh57"
      },
      "source": [
        "### Trace order\n",
        "If you are worried about some random numbers matching in `constant` mode reducing your confidence in your numerical equivalence test (for example if several weights take on the same initializations), you can use the `num_random_ops` mode to avoid this. In the `num_random_ops` mode, the generated random numbers will depend on the ordering of random ops in the program."
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "metadata": {
        "id": "L-AeD148VygJ"
      },
      "outputs": [],
      "source": [
        "random_tool = v1.keras.utils.DeterministicRandomTestTool(mode='num_random_ops')\n",
        "with random_tool.scope():\n",
        "  graph = tf.Graph()\n",
        "  with graph.as_default(), tf.compat.v1.Session(graph=graph) as sess:\n",
        "    a = tf.random.uniform(shape=(3,1))\n",
        "    a = a * 3\n",
        "    b = tf.random.uniform(shape=(3,3))\n",
        "    b = b * 3\n",
        "    c = tf.random.uniform(shape=(3,3))\n",
        "    c = c * 3\n",
        "    graph_a, graph_b, graph_c = sess.run([a, b, c])\n",
        "\n",
        "graph_a, graph_b, graph_c"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "metadata": {
        "id": "CedD41NuVygK"
      },
      "outputs": [],
      "source": [
        "random_tool = v1.keras.utils.DeterministicRandomTestTool(mode='num_random_ops')\n",
        "with random_tool.scope():\n",
        "  a = tf.random.uniform(shape=(3,1))\n",
        "  a = a * 3\n",
        "  b = tf.random.uniform(shape=(3,3))\n",
        "  b = b * 3\n",
        "  c = tf.random.uniform(shape=(3,3))\n",
        "  c = c * 3\n",
        "\n",
        "a, b, c"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "metadata": {
        "id": "5We2xSnLVygL"
      },
      "outputs": [],
      "source": [
        "# Demonstrate that the generated random numbers match\n",
        "np.testing.assert_allclose(graph_a, a.numpy(), **tol_dict)\n",
        "np.testing.assert_allclose(graph_b, b.numpy(), **tol_dict )\n",
        "np.testing.assert_allclose(graph_c, c.numpy(), **tol_dict)"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "metadata": {
        "id": "BBFG1xehWneM"
      },
      "outputs": [],
      "source": [
        "# Demonstrate that with the 'num_random_ops' mode,\n",
        "# b & c took on different values even though\n",
        "# their generated shape was the same\n",
        "assert not np.allclose(b.numpy(), c.numpy(), **tol_dict)"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {
        "id": "OfX_VexcVqSA"
      },
      "source": [
        "However, notice that in this mode random generation is sensitive to program order, and so the following generated random numbers do not match."
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "metadata": {
        "id": "cZt__ElEIDl_"
      },
      "outputs": [],
      "source": [
        "random_tool = v1.keras.utils.DeterministicRandomTestTool(mode='num_random_ops')\n",
        "with random_tool.scope():\n",
        "  a = tf.random.uniform(shape=(3,1))\n",
        "  a = a * 3\n",
        "  b = tf.random.uniform(shape=(3,3))\n",
        "  b = b * 3\n",
        "\n",
        "random_tool = v1.keras.utils.DeterministicRandomTestTool(mode='num_random_ops')\n",
        "with random_tool.scope():\n",
        "  b_prime = tf.random.uniform(shape=(3,3))\n",
        "  b_prime = b_prime * 3\n",
        "  a_prime = tf.random.uniform(shape=(3,1))\n",
        "  a_prime = a_prime * 3\n",
        "\n",
        "assert not np.allclose(a.numpy(), a_prime.numpy())\n",
        "assert not np.allclose(b.numpy(), b_prime.numpy())"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {
        "id": "nHhOLHyQIkAe"
      },
      "source": [
        "To allow for debugging variations due to tracing order, `DeterministicRandomTestTool` in `num_random_ops` mode allows you to see how many random operations have been traced with the `operation_seed` property."
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "metadata": {
        "id": "33RCSICuJEyV"
      },
      "outputs": [],
      "source": [
        "random_tool = v1.keras.utils.DeterministicRandomTestTool(mode='num_random_ops')\n",
        "with random_tool.scope():\n",
        "  print(random_tool.operation_seed)\n",
        "  a = tf.random.uniform(shape=(3,1))\n",
        "  a = a * 3\n",
        "  print(random_tool.operation_seed)\n",
        "  b = tf.random.uniform(shape=(3,3))\n",
        "  b = b * 3\n",
        "  print(random_tool.operation_seed)"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {
        "id": "bkQD3NpOMxIv"
      },
      "source": [
        "If you need to account for varying trace order in your tests, you can even set the auto-incrementing `operation_seed` explicitly. For example, you can use this to make random number generation match across two different program orders."
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "metadata": {
        "id": "6W4sS_wOM8CH"
      },
      "outputs": [],
      "source": [
        "random_tool = v1.keras.utils.DeterministicRandomTestTool(mode='num_random_ops')\n",
        "with random_tool.scope():\n",
        "  print(random_tool.operation_seed)\n",
        "  a = tf.random.uniform(shape=(3,1))\n",
        "  a = a * 3\n",
        "  print(random_tool.operation_seed)\n",
        "  b = tf.random.uniform(shape=(3,3))\n",
        "  b = b * 3\n",
        "\n",
        "random_tool = v1.keras.utils.DeterministicRandomTestTool(mode='num_random_ops')\n",
        "with random_tool.scope():\n",
        "  random_tool.operation_seed = 1\n",
        "  b_prime = tf.random.uniform(shape=(3,3))\n",
        "  b_prime = b_prime * 3\n",
        "  random_tool.operation_seed = 0\n",
        "  a_prime = tf.random.uniform(shape=(3,1))\n",
        "  a_prime = a_prime * 3\n",
        "\n",
        "np.testing.assert_allclose(a.numpy(), a_prime.numpy(), **tol_dict)\n",
        "np.testing.assert_allclose(b.numpy(), b_prime.numpy(), **tol_dict)\n"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {
        "id": "bP5Kx1OcNbvM"
      },
      "source": [
        "However, `DeterministicRandomTestTool` disallows reusing already-used operation seeds, so make sure the auto-incremented sequences cannot overlap. This is because eager execution generates different numbers for follow-on usages of the same operation seed while TF1 graphs and sessions do not, so raising an error helps keep session and eager stateful random number generation in line."
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "metadata": {
        "id": "GmBgg5hzNa5H"
      },
      "outputs": [],
      "source": [
        "random_tool = v1.keras.utils.DeterministicRandomTestTool(mode='num_random_ops')\n",
        "with random_tool.scope():\n",
        "  random_tool.operation_seed = 1\n",
        "  b_prime = tf.random.uniform(shape=(3,3))\n",
        "  b_prime = b_prime * 3\n",
        "  random_tool.operation_seed = 0\n",
        "  a_prime = tf.random.uniform(shape=(3,1))\n",
        "  a_prime = a_prime * 3\n",
        "  try:\n",
        "    c = tf.random.uniform(shape=(3,1))\n",
        "    raise RuntimeError(\"An exception should have been raised before this, \" +\n",
        "                     \"because the auto-incremented operation seed will \" +\n",
        "                     \"overlap an already-used value\")\n",
        "  except ValueError as err:\n",
        "    print(err)\n"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {
        "id": "U-bLOeCmOn-4"
      },
      "source": [
        "### Verifying Inference\n",
        "\n",
        "You can now use the `DeterministicRandomTestTool` to make sure the `InceptionResnetV2` model matches in inference, even when using the random weight initialization. For a stronger test condition due to matching program order, use the `num_random_ops` mode."
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "metadata": {
        "id": "8TWOrflkPa7T"
      },
      "outputs": [],
      "source": [
        "random_tool = v1.keras.utils.DeterministicRandomTestTool(mode='num_random_ops')\n",
        "with random_tool.scope():\n",
        "  graph = tf.Graph()\n",
        "  with graph.as_default(), tf.compat.v1.Session(graph=graph) as sess:\n",
        "    height, width = 299, 299\n",
        "    num_classes = 1000\n",
        "    inputs = tf.ones( (1, height, width, 3))\n",
        "\n",
        "    out, endpoints = inception_resnet_v2(inputs, num_classes, is_training=False)\n",
        "\n",
        "    # Initialize the variables\n",
        "    sess.run(tf.compat.v1.global_variables_initializer())\n",
        "\n",
        "    # Grab the outputs & regularization loss\n",
        "    reg_losses = tf.compat.v1.get_collection(tf.compat.v1.GraphKeys.REGULARIZATION_LOSSES)\n",
        "    tf1_regularization_loss = sess.run(tf.math.add_n(reg_losses))\n",
        "    tf1_output = sess.run(out)\n",
        "\n",
        "  print(\"Regularization loss:\", tf1_regularization_loss)"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "metadata": {
        "id": "Qcx6ur4KPMI1"
      },
      "outputs": [],
      "source": [
        "height, width = 299, 299\n",
        "num_classes = 1000\n",
        "\n",
        "random_tool = v1.keras.utils.DeterministicRandomTestTool(mode='num_random_ops')\n",
        "with random_tool.scope():\n",
        "  model = InceptionResnetV2(num_classes)\n",
        "\n",
        "  inputs = tf.ones((1, height, width, 3))\n",
        "  tf2_output, endpoints = model(inputs, training=False)\n",
        "\n",
        "  # Grab the regularization loss as well\n",
        "  tf2_regularization_loss = tf.math.add_n(model.losses)\n",
        "\n",
        "print(\"Regularization loss:\", tf2_regularization_loss)"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "metadata": {
        "id": "m_SS2b6qPFl1"
      },
      "outputs": [],
      "source": [
        "# Verify that the regularization loss and output both match\n",
        "# when using the DeterministicRandomTestTool:\n",
        "np.testing.assert_allclose(tf1_regularization_loss, tf2_regularization_loss.numpy(), **tol_dict)\n",
        "np.testing.assert_allclose(tf1_output, tf2_output.numpy(), **tol_dict)"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {
        "id": "TKSktIRaP-5b"
      },
      "source": [
        "### Verifying Training\n",
        "\n",
        "Because `DeterministicRandomTestTool` works for *all* stateful random operations (including both weight initialization and computation such as dropout layers), you can use it to verify the models match in training mode as well. You can again use the `num_random_ops` mode because the program order of the stateful random ops matches."
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "metadata": {
        "id": "nMBFVa1kQTJH"
      },
      "outputs": [],
      "source": [
        "random_tool = v1.keras.utils.DeterministicRandomTestTool(mode='num_random_ops')\n",
        "with random_tool.scope():\n",
        "  graph = tf.Graph()\n",
        "  with graph.as_default(), tf.compat.v1.Session(graph=graph) as sess:\n",
        "    height, width = 299, 299\n",
        "    num_classes = 1000\n",
        "    inputs = tf.ones( (1, height, width, 3))\n",
        "\n",
        "    out, endpoints = inception_resnet_v2(inputs, num_classes, is_training=True)\n",
        "\n",
        "    # Initialize the variables\n",
        "    sess.run(tf.compat.v1.global_variables_initializer())\n",
        "\n",
        "    # Grab the outputs & regularization loss\n",
        "    reg_losses = tf.compat.v1.get_collection(tf.compat.v1.GraphKeys.REGULARIZATION_LOSSES)\n",
        "    tf1_regularization_loss = sess.run(tf.math.add_n(reg_losses))\n",
        "    tf1_output = sess.run(out)\n",
        "\n",
        "  print(\"Regularization loss:\", tf1_regularization_loss)"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "metadata": {
        "id": "-jlBkwI5QTJI"
      },
      "outputs": [],
      "source": [
        "height, width = 299, 299\n",
        "num_classes = 1000\n",
        "\n",
        "random_tool = v1.keras.utils.DeterministicRandomTestTool(mode='num_random_ops')\n",
        "with random_tool.scope():\n",
        "  model = InceptionResnetV2(num_classes)\n",
        "\n",
        "  inputs = tf.ones((1, height, width, 3))\n",
        "  tf2_output, endpoints = model(inputs, training=True)\n",
        "\n",
        "  # Grab the regularization loss as well\n",
        "  tf2_regularization_loss = tf.math.add_n(model.losses)\n",
        "\n",
        "print(\"Regularization loss:\", tf2_regularization_loss)"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "metadata": {
        "id": "IL9mjTLnQTJJ"
      },
      "outputs": [],
      "source": [
        "# Verify that the regularization loss and output both match\n",
        "# when using the DeterministicRandomTestTool\n",
        "np.testing.assert_allclose(tf1_regularization_loss, tf2_regularization_loss.numpy(), **tol_dict)\n",
        "np.testing.assert_allclose(tf1_output, tf2_output.numpy(), **tol_dict)"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {
        "id": "uJTZvmfnQqZH"
      },
      "source": [
        "You have now verified that the `InceptionResnetV2` model running eagerly with decorators around `tf.keras.layers.Layer` numerically matches the slim network running in TF1 graphs and sessions."
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {
        "id": "xpOAei5vRAPa"
      },
      "source": [
        "Note: When using the `DeterministicRandomTestTool` in `num_random_ops` mode, it is suggested you directly use and call the `tf.keras.layers.Layer` method decorator when testing for numerical equivalence. Embedding it within a Keras functional model or other Keras models can produce differences in stateful random operation tracing order that can be tricky to reason about or match exactly when comparing TF1.x graphs/sessions and eager execution. \n",
        "\n",
        "For example, calling the `InceptionResnetV2` layer directly with `training=True` interleaves variable initialization with the dropout order according to the network creation order.\n",
        "\n",
        "On the other hand, first putting the `tf.keras.layers.Layer` decorator in a Keras functional model and only then calling the model with `training=True` is equivalent to initializing all variables then using the dropout layer. This produces a different tracing order and a different set of random numbers.\n",
        "\n",
        "However, the default `mode='constant'` is not sensitive to these differences in tracing order and will pass without extra work even when embedding the layer in a Keras functional model."
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "metadata": {
        "id": "0dSR4ZNvYNYm"
      },
      "outputs": [],
      "source": [
        "random_tool = v1.keras.utils.DeterministicRandomTestTool()\n",
        "with random_tool.scope():\n",
        "  graph = tf.Graph()\n",
        "  with graph.as_default(), tf.compat.v1.Session(graph=graph) as sess:\n",
        "    height, width = 299, 299\n",
        "    num_classes = 1000\n",
        "    inputs = tf.ones( (1, height, width, 3))\n",
        "\n",
        "    out, endpoints = inception_resnet_v2(inputs, num_classes, is_training=True)\n",
        "\n",
        "    # Initialize the variables\n",
        "    sess.run(tf.compat.v1.global_variables_initializer())\n",
        "\n",
        "    # Get the outputs & regularization losses\n",
        "    reg_losses = tf.compat.v1.get_collection(tf.compat.v1.GraphKeys.REGULARIZATION_LOSSES)\n",
        "    tf1_regularization_loss = sess.run(tf.math.add_n(reg_losses))\n",
        "    tf1_output = sess.run(out)\n",
        "\n",
        "  print(\"Regularization loss:\", tf1_regularization_loss)"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "metadata": {
        "id": "iMPMMnPtYUY7"
      },
      "outputs": [],
      "source": [
        "height, width = 299, 299\n",
        "num_classes = 1000\n",
        "\n",
        "random_tool = v1.keras.utils.DeterministicRandomTestTool()\n",
        "with random_tool.scope():\n",
        "  keras_input = tf.keras.Input(shape=(height, width, 3))\n",
        "  layer = InceptionResnetV2(num_classes)\n",
        "  model = tf.keras.Model(inputs=keras_input, outputs=layer(keras_input))\n",
        "\n",
        "  inputs = tf.ones((1, height, width, 3))\n",
        "  tf2_output, endpoints = model(inputs, training=True)\n",
        "\n",
        "  # Get the regularization loss\n",
        "  tf2_regularization_loss = tf.math.add_n(model.losses)\n",
        "\n",
        "print(\"Regularization loss:\", tf2_regularization_loss)"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "metadata": {
        "id": "jf46KUVyYUY8"
      },
      "outputs": [],
      "source": [
        "# Verify that the regularization loss and output both match\n",
        "# when using the DeterministicRandomTestTool\n",
        "np.testing.assert_allclose(tf1_regularization_loss, tf2_regularization_loss.numpy(), **tol_dict)\n",
        "np.testing.assert_allclose(tf1_output, tf2_output.numpy(), **tol_dict)"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {
        "id": "hWXHjtkiZ09V"
      },
      "source": [
        "## Step 3b or 4b (optional): Testing with pre-existing checkpoints\n",
        "\n",
        "After step 3 or step 4 above, it can be useful to run your numerical equivalence tests when starting from pre-existing name-based checkpoints if you have some. This can test both that your legacy checkpoint loading is working correctly and that the model itself is working right. The [Reusing TF1.x checkpoints guide](./reuse_checkpoints.ipynb) covers how to reuse your pre-existing TF1.x checkpoints and transfer them over to TF2 checkpoints.\n"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {
        "id": "v6i3MFmGcxYx"
      },
      "source": [
        "## Additional Testing & Troubleshooting\n",
        "\n",
        "As you add more numerical equivalence tests, you may also choose to add a test that verifies your gradient computation (or even your optimizer updates) match.\n",
        "\n",
        "Backpropagation and gradient computation are more prone to floating point numerical instabilities than model forward passes. This means that as your equivalence tests cover more non-isolated parts of your training, you may begin to see non-trivial numerics differences between running fully eagerly and your TF1 graphs. This may be caused by TensorFlow's graph optimizations that do things such as replace subexpressions in a graph with fewer mathematical operations.\n",
        "\n",
        "To isolate whether this is likely to be the case, you can compare your TF1 code to TF2 computation happening inside of a `tf.function` (which applies graph optimization passes like your TF1 graph) rather than to a purely eager computation. Alternatively, you can try using `tf.config.optimizer.set_experimental_options` to disable optimization passes such as `\"arithmetic_optimization\"` before your TF1 computation to see if the result ends up numerically closer to your TF2 computation results. In your actual training runs it is recommended you use `tf.function` with optimization passes enabled for performance reasons, but you may find it useful to disable them in your numerical equivalence unit tests.\n",
        "\n",
        "Similarly, you may also find that `tf.compat.v1.train` optimizers and TF2 optimizers have slightly different floating point numerics properties than TF2 optimizers, even if the mathematical formulas they are representing are the same. This is less likely to be an issue in your training runs, but it may require a higher numerical tolerance in equivalence unit tests."
      ]
    }
  ],
  "metadata": {
    "colab": {
      "collapsed_sections": [],
      "name": "validate_correctness.ipynb",
      "toc_visible": true
    },
    "kernelspec": {
      "display_name": "Python 3",
      "name": "python3"
    }
  },
  "nbformat": 4,
  "nbformat_minor": 0
}
